home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / python-support / gnome-games-data / glchess / chess / san.py < prev   
Encoding:
Python Source  |  2009-04-14  |  16.0 KB  |  450 lines

  1. # -*- coding: utf-8 -*-
  2. """
  3. """
  4.  
  5. __author__ = 'Robert Ancell <bob27@users.sourceforge.net>'
  6. __license__ = 'GNU General Public License Version 2'
  7. __copyright__ = 'Copyright 2005-2006  Robert Ancell'
  8.  
  9. __all__ = ['SANConverter']
  10.  
  11. # Examples of SAN moves:
  12. #
  13. # f4      (pawn move to f4)
  14. # fxg3    (pawn on file f takes opponent on g3)
  15. # Qh5     (queen moves to h5)
  16. # Qh5+    (queen moves to h5 and puts opponent into check)
  17. # Ned4    (knight on file e moves to d4)
  18. # gxh8=Q# (pawn on g7 takes opponent in h8 promotes to queen and puts oponent into checkmate (smooth!))
  19.  
  20. # Notation for takes
  21. SAN_TAKE   = 'x'
  22.  
  23. # Castling moves
  24. SAN_CASTLE_SHORT = 'O-O'
  25. SAN_CASTLE_LONG  = 'O-O-O'
  26.  
  27. RANKS = 'abcdefgh'
  28. FILES = '12345678'
  29.  
  30. # Suffixes
  31. SAN_PROMOTE   = '='
  32.  
  33. class Error(Exception):
  34.     """
  35.     """
  36.     pass
  37.  
  38. class SANConverter:
  39.     """
  40.     
  41.     Define file and rank
  42.     """
  43.     
  44.     # Piece colours
  45.     WHITE = 'White'
  46.     BLACK = 'Black'
  47.     
  48.     # SAN piece types
  49.     PAWN   = 'P'
  50.     KNIGHT = 'N'
  51.     BISHOP = 'B'
  52.     ROOK   = 'R'
  53.     QUEEN  = 'Q'
  54.     KING   = 'K'
  55.     __pieceTypes = PAWN + KNIGHT + BISHOP + ROOK + QUEEN + KING
  56.     
  57.     # Valid promotion types
  58.     __promotionTypes = PAWN + KNIGHT + BISHOP + ROOK + QUEEN
  59.     
  60.     # Move results
  61.     CHECK     = '+'
  62.     CHECKMATE = '#'
  63.     
  64.     def __init__(self):
  65.         """Constructor"""
  66.         pass
  67.     
  68.     # Methods to extend
  69.     
  70.     def getPiece(self, location):
  71.         """Get a piece from the chess board.
  72.         
  73.         'location' is the location to get the piece from (string, e.g. 'a1', h8').
  74.         
  75.         Return a tuple containing (colour, type) or None if no piece at this location.
  76.         """
  77.         return None
  78.     
  79.     def testMove(self, colour, start, end, promotionType, allowSuicide = False):
  80.         """Test if a move is valid.
  81.         
  82.         'colour' is the colour of the player making the move (self.WHITE or self.BLACK).
  83.         'start' is the board location to move from (string, e.g. 'a1', 'h8').
  84.         'end' is the board location to move to (string, e.g. 'a1', 'h8').
  85.         'promotionType' is the piece type to promote to (self.[PAWN|KNIGHT|BISHOP|ROOK|QUEEN]).
  86.         'allowSuicide' is a flag to show if the move should be disallowed (False) or
  87.                        allowed (True) if it would put the moving player into check.
  88.  
  89.         Return False if the move is dissallowed or
  90.                self.CHECK if the move puts the opponent into check or
  91.                self.CHECKMATE if the move puts the opponent into checkmate or
  92.                True if the move is allowed and does not put the opponent into check.
  93.         """
  94.         pass
  95.     
  96.     def decode(self, colour, san):
  97.         """Decode a SAN move.
  98.         
  99.         'colour' is the colour of the player making the move (self.WHITE or self.BLACK).
  100.         'san' is the SAN description of the move (string).
  101.         
  102.         Returns the move this SAN describes in the form (start, end, promotionType).
  103.         'start' is the square to move from (string, e.g. 'a1', 'h8').
  104.         'end' is the square to move to (string, e.g. 'a1', 'h8').
  105.         'promotionType' is the piece to promote to (self.[KNIGHT|BISHOP|ROOK|QUEEN]).
  106.         If the move is invalid then an Error expection is raised.
  107.         """
  108.         copy = san[:]
  109.         
  110.         # Look for check hints
  111.         expectedResult = True
  112.         if copy[-1] == self.CHECK or copy[-1] == self.CHECKMATE:
  113.             expectedResult = copy[-1]
  114.             copy = copy[:-1]
  115.  
  116.         # Extract promotions
  117.         promotionType = self.QUEEN
  118.         if copy[-2] == SAN_PROMOTE:
  119.             promotionType = copy[-1]
  120.             copy = copy[:-2]
  121.             if self.__promotionTypes.find(promotionType) < 0:
  122.                 raise Error("Error decoding '%s', Invalid promotion type %s" % (repr(san), repr(promotionType)))
  123.             
  124.         # Some people miss out the '='
  125.         elif self.__promotionTypes.find(copy[-1]) >= 0:
  126.             promotionType = copy[-1]
  127.             copy = copy[:-1]
  128.  
  129.         # Check for castling moves
  130.         if colour is self.WHITE:
  131.             baseFile = '1'
  132.         else:
  133.             baseFile = '8'
  134.         # FIXME: Update moveResult and compare against expectedResult
  135.         if copy == SAN_CASTLE_SHORT:
  136.             return ('e' + baseFile, 'g' + baseFile, expectedResult, promotionType)
  137.         elif copy == SAN_CASTLE_LONG:
  138.             return ('e' + baseFile, 'c' + baseFile, expectedResult, promotionType)
  139.  
  140.         # Get the destination (the last two characters before the suffix)
  141.         end = copy[-2:]
  142.         copy = copy[:-2]
  143.         if RANKS.find(end[0]) < 0 or FILES.find(end[1]) < 0:
  144.             raise Error("Error decoding '%s', Invalid destination type %s" % (repr(san), repr(end)))
  145.  
  146.         # Check if is a take move (use try in case there are no more characters)
  147.         isTake = False
  148.         try:
  149.             if copy[-1] == SAN_TAKE:
  150.                 isTake = True
  151.                 copy = copy[:-1]
  152.         except:
  153.             pass
  154.  
  155.         # The first character is the piece type (or pawn if not specified)
  156.         pieceType = self.PAWN
  157.         if len(copy) > 0:
  158.             if self.__pieceTypes.find(copy[0]) >= 0:
  159.                 pieceType = copy[0]
  160.                 copy = copy[1:]
  161.  
  162.         # Get the rank of the source piece (if supplied)
  163.         rank = None
  164.         if len(copy) > 0:
  165.             if RANKS.find(copy[0]) >= 0:
  166.                 rank = copy[0]
  167.                 copy = copy[1:]
  168.  
  169.         # Get the file of the source piece (if supplied)
  170.         file = None
  171.         if len(copy) > 0:
  172.             if FILES.find(copy[0]) >= 0:
  173.                 file = copy[0]
  174.                 copy = copy[1:]
  175.  
  176.         # There should be no more characters
  177.         if len(copy) != 0:
  178.             raise Error('Error decoding %s, Unexpected extra characters %s' % (repr(san), repr(end)))
  179.  
  180.         # If have both rank and file for source then we have the move completely defined
  181.         moveResult = None
  182.         move = None
  183.         if rank is not None and file is not None:
  184.             start = rank + file
  185.             moveResult = self.testMove(colour, start, end, promotionType = promotionType)
  186.             move = (start, end)
  187.         else:
  188.             # Try and find a piece that matches the source one
  189.             if file is None:
  190.                 fileRange = FILES
  191.             else:
  192.                 fileRange = file
  193.             if rank is None:
  194.                 rankRange = RANKS
  195.             else:
  196.                 rankRange = rank
  197.             
  198.             for file in fileRange:
  199.                 for rank in rankRange:
  200.                     start = rank + file
  201.                 
  202.                     # Only test our pieces
  203.                     piece = self.getPiece(start)
  204.                     if piece is None:
  205.                         continue
  206.                     if piece[0] != colour or piece[1] != pieceType:
  207.                         continue
  208.  
  209.                     # If move is valid this is a possible move
  210.                     # FIXME: Check the crafty case in reverse (i.e. suicidal moves)
  211.                     result = self.testMove(colour, start, end, promotionType = promotionType)
  212.                     if result is False:
  213.                         continue
  214.                 
  215.                     # Multiple matches
  216.                     if moveResult is not None:
  217.                         raise Error('Error decoding %s, Move is ambiguous, at least %s and %s are possible' % (repr(san), repr(move), repr([start, end])))
  218.                     moveResult = result
  219.                     move = [start, end]
  220.  
  221.         # Failed to find a match
  222.         if moveResult is None:
  223.             raise Error('Error decoding %s, Not a valid move' % repr(san))
  224.  
  225.         return (move[0], move[1], expectedResult, promotionType)
  226.  
  227.     def encode(self, start, end, isTake = False, promotionType = QUEEN):
  228.         """Convert glChess co-ordinate move to SAN notation.
  229.  
  230.         'start' is the square to move from (string, e.g. 'a1', 'h8').
  231.         'end' is the square to move to (string, e.g. 'a1', 'h8').
  232.         'promotionType' is the piece used for pawn promotion (if necessary).
  233.  
  234.         Return the move in SAN notation or None if unable to convert.
  235.         """
  236.         piece = self.getPiece(start)
  237.         if piece is None:
  238.             raise Error('Encode error, no piece to move at %s' % repr(start))
  239.         (pieceColour, pieceType) = piece
  240.  
  241.         # Test the move is valid
  242.         if self.testMove(pieceColour, start, end, promotionType) is False:
  243.             raise Error('Encode error, move %s%s is invalid' % (start, end))
  244.  
  245.         # Check for castling
  246.         if pieceType is self.KING:
  247.             # Castling
  248.             if pieceColour is self.WHITE:
  249.                 baseFile = '1'
  250.             else:
  251.                 baseFile = '8'
  252.             shortCastle = ('e' + baseFile, 'g' + baseFile)
  253.             longCastle = ('e' + baseFile, 'c' + baseFile)
  254.  
  255.             san = None
  256.             if (start, end) == shortCastle:
  257.                 san = SAN_CASTLE_SHORT
  258.             elif (start, end) == longCastle:
  259.                 san = SAN_CASTLE_LONG
  260.             if san is not None:
  261.                 result = self.testMove(pieceColour, start, end, promotionType = promotionType, allowSuicide = True)
  262.                 if result is self.CHECK:
  263.                     san += self.CHECK
  264.                 elif result is self.CHECKMATE:
  265.                     san += self.CHECKMATE
  266.                 return san
  267.  
  268.         # Try and describe this piece with the minimum of information
  269.         file = '?'
  270.         rank = '?'
  271.     
  272.         # Pawns always explicitly provide rank when taking
  273.         if pieceType is self.PAWN and isTake:
  274.             rank = start[0]
  275.  
  276.         # First try no rank or file, then just file, then just rank, then both
  277.         result = self.__isUnique(pieceColour, pieceType, rank + file, end, promotionType)
  278.         if result is None:
  279.             # Try with rank
  280.             rank = start[0]
  281.             file = '?'
  282.             result = self.__isUnique(pieceColour, pieceType, rank + '?', end, promotionType)
  283.  
  284.             if result is None:
  285.                 # Try with file
  286.                 rank = '?'
  287.                 file = start[1]
  288.                 result = self.__isUnique(pieceColour, pieceType, '?' + file, end, promotionType)
  289.  
  290.                 if result is None:
  291.                     # Try with rank and file
  292.                     result = self.__isUnique(pieceColour, pieceType, rank + file, end, promotionType)
  293.             
  294.                     # This move is illegal
  295.                     if result is None:
  296.                         raise Error('Encode error, unable to find unique move for %s%s' % (start, end))
  297.  
  298.         # Store the piece that is being moved, note pawns are not marked
  299.         san = ''
  300.         if pieceType is not self.PAWN:
  301.             san += pieceType
  302.  
  303.         # Disambiguations
  304.         if rank != '?':
  305.             san += rank
  306.         if file != '?':
  307.             san += file
  308.  
  309.         # Mark if taking a piece
  310.         if isTake:
  311.             san += SAN_TAKE
  312.  
  313.         # Write target co-ordinate
  314.         san += end
  315.  
  316.         # If a pawn promotion record the type
  317.         if pieceColour is self.WHITE:
  318.             promotionFile = '8'
  319.         else:
  320.             promotionFile = '1'
  321.         if pieceType == self.PAWN and end[1] == promotionFile:
  322.             san += SAN_PROMOTE + promotionType
  323.  
  324.         # Record if this is a check/checkmate move
  325.         if result is self.CHECK:
  326.             san += self.CHECK
  327.         elif result is self.CHECKMATE:
  328.             san += self.CHECKMATE
  329.  
  330.         return san
  331.     
  332.     def __isUnique(self, colour, pieceType, start, end, promotionType = QUEEN):
  333.         """Test if a move is unique.
  334.     
  335.         'colour' is the piece colour being moved. (self.WHITE or self.BLACK).
  336.         'pieceType' is the type of the piece being moved (self.[PAWN|KNIGHT|BISHOP|ROOK|QUEEN|KING]).
  337.         'start' is the start location of the move (tuple (file, rank). rank and file can be None).
  338.         'end' is the end point of the move (tuple (file,rank)).
  339.         'promotionType' is the piece type to promote pawns to (self.[PAWN|KNIGHT|BISHOP|ROOK|QUEEN]).
  340.     
  341.         Return the result of self.testMove() if a unique move is found otherwise None.
  342.         """
  343.         lastResult = None
  344.     
  345.         # Work out what ranges to iterate over
  346.         if start[0] == '?':
  347.             rankRange = RANKS
  348.         else:
  349.             rankRange = start[0]
  350.         if start[1] == '?':
  351.             fileRange = FILES
  352.         else:
  353.             fileRange = start[1]
  354.     
  355.         for file in fileRange:
  356.             for rank in rankRange:
  357.                 # Check if there is a piece of this type and colour at this location
  358.                 p = self.getPiece(rank + file)
  359.                 if p is None:
  360.                     continue
  361.                 if p[1] != pieceType or p[0] != colour:
  362.                     continue
  363.  
  364.                 # If move is valid this is a possible move
  365.                 # NOTE: We check moves that would be suicide for us otherwise crafty claims they
  366.                 # are ambiguous, the PGN specification says we don't need to disambiguate if only
  367.                 # one non-suicidal move is available.
  368.                 # (8.2.3.4: Disambiguation) */
  369.                 result = self.testMove(colour, rank + file, end, promotionType = promotionType, allowSuicide = True)
  370.                 if result is not False:
  371.                     # If multiple matches then not unique (duh!)
  372.                     if lastResult != None:
  373.                         return None
  374.                     lastResult = result
  375.  
  376.         # Return the result of the move
  377.         return lastResult
  378.  
  379. if __name__ == '__main__':
  380.     
  381.     import chess_board
  382.     
  383.     class TestConverter(SANConverter):
  384.         """
  385.         """
  386.         
  387.         __colourToSAN = {chess_board.WHITE: SANConverter.WHITE, chess_board.BLACK: SANConverter.BLACK}
  388.         __sanToColour = {}
  389.         for (a, b) in __colourToSAN.iteritems():
  390.             __sanToColour[b] = a
  391.         
  392.         __typeToSAN = {chess_board.PAWN: SANConverter.PAWN,
  393.                        chess_board.KNIGHT: SANConverter.KNIGHT,
  394.                        chess_board.BISHOP: SANConverter.BISHOP,
  395.                        chess_board.ROOK: SANConverter.ROOK,
  396.                        chess_board.QUEEN: SANConverter.QUEEN,
  397.                        chess_board.KING: SANConverter.KING}
  398.         __sanToType = {}
  399.         for (a, b) in __typeToSAN.iteritems():
  400.             __sanToType[b] = a
  401.         
  402.         __board = None
  403.         
  404.         def __init__(self, board):
  405.             self.__board = board
  406.             
  407.         def testEncode(self, start, end):
  408.             print str((start, end)) + ' => ' + str(self.encode(start, end))
  409.             
  410.         def testDecode(self, colour, san):
  411.             try:
  412.                 result = self.decode(colour, san)
  413.                 print san.ljust(7) + ' => ' + str(result)
  414.             except Error, e:
  415.                 print san.ljust(7) + ' !! ' + str(e)
  416.         
  417.         def getPiece(self, file, rank):
  418.             """Called by SANConverter"""
  419.             piece = self.__board.getPiece((file, rank))
  420.             if piece is None:
  421.                 return None
  422.             return (self.__colourToSAN[piece.getColour()], self.__typeToSAN[piece.getType()])
  423.  
  424.         def testMove(self, colour, start, end, promotionType, allowSuicide = False):
  425.             """Called by SANConverter"""
  426.             moveResult = self.__board.testMove(self.__sanToColour[colour], ((start, end)), self.__sanToType[promotionType], allowSuicide)
  427.             
  428.             return {chess_board.MOVE_RESULT_ILLEGAL: False,
  429.                     chess_board.MOVE_RESULT_ALLOWED: True,
  430.                     chess_board.MOVE_RESULT_OPPONENT_CHECK: self.CHECK,
  431.                     chess_board.MOVE_RESULT_OPPONENT_CHECKMATE: self.CHECKMATE}[moveResult]
  432.  
  433.     b = chess_board.ChessBoard()
  434.     c = TestConverter(b)
  435.     
  436.     print b
  437.     c.testEncode((1,1), (1,2))
  438.     c.testEncode((1,0), (2,2))
  439.     
  440.     c.testDecode(c.WHITE, 'c3')    # Simple pawn move
  441.     c.testDecode(c.WHITE, 'Pc3')   # Explicit pawn move
  442.     c.testDecode(c.WHITE, 'c4')    # Pawn march
  443.     c.testDecode(c.WHITE, 'Nc3')   # Non-pawn move
  444.     c.testDecode(c.WHITE, 'Qd3')   # Invalid move
  445.     c.testDecode(c.WHITE, 'Qd3=X') # Invalid promotion type
  446.     c.testDecode(c.WHITE, 'x3')    # Invalid destination
  447.     c.testDecode(c.WHITE, 'ic3')   # Extra characters
  448.     # TODO: Ambiguous move
  449.     print b
  450.